package unifor.mia.redes.relprotocol.channel;

import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;

import unifor.mia.redes.relprotocol.msg.ACK;
import unifor.mia.redes.relprotocol.msg.Pacote;

public class Channel {

	private static final long serialVersionUID = -1502215318788279499L;
	

	private static int ATRASO_ACK = 1;
	private static int ERRO_ACK = 2;
	private static int ERRO_MENSAGEM = 3;
	private static int PERDA_PACOTE = 4;
	
	private static boolean errosACK = false;
	private static boolean atrasoACK = false;
	private static boolean simularErro = false;
	private static boolean perderPacote = false;

	private static int atraso = 200;
	
	private static final LinkedBlockingQueue<Pacote> commChannel = new LinkedBlockingQueue<Pacote>(); 
	private static final LinkedBlockingQueue<ACK> controlChannel = new LinkedBlockingQueue<ACK>(); 
	
	private static final Random randomGenerator = new Random();

	public void usarErrosACK(boolean usar){
		errosACK = usar;
	}
	
	public void usarAtrasosACK(boolean usar){
		atrasoACK = usar;
	}

	public static void write(Pacote msg){

		try {
			Pacote m;
			// Para simular um canal de comunicação não confiável, inserimos erros randomicamente nos pacotes
			if(simularErro){
				m = inserirErro(msg);
			} else {
				m = msg;
			}
//			System.out.println("[CHANNEL] Mensagem enviada: #(" + m.getSeq() + ") => HEADER(" + m.getHeader() + ") => BODY(" + m.getBody() + ") => HASH(" + m.getHash() + ")");
			System.out.println("[CHANNEL] Mensagem enviada: #(" + m.getSeq() + ") => HEADER(" + m.getHeader() + ") => BODY(" + m.getBody() + ")");
			
			if(perderPacote && ocorrenciaProbabilidade(PERDA_PACOTE)) {
				System.out.println("[CHANNEL] Pacote perdido no canal");
			} else {
				commChannel.put(m);
			}
		} catch (InterruptedException e) {
			//nada a fazer se thread for interrompida
			return;
		}
	}

	public static Pacote read(){
		try {
			Pacote m = commChannel.take();
//			System.out.println("[CHANNEL] Mensagem recebida: #(" + m.getSeq() + ") => HEADER(" + m.getHeader() + ") => BODY(" + m.getBody() + ") => HASH(" + m.getHash() + ")");
			System.out.println("[CHANNEL] Mensagem recebida: #(" + m.getSeq() + ") => HEADER(" + m.getHeader() + ") => BODY(" + m.getBody() + ")");
			return m;
		} catch (InterruptedException e) {
			Pacote m = new Pacote();
			m.setBody("");
			return m;
		}
	}

	private static Pacote inserirErro(Pacote msg) {
		
		// Se ocorreu incidencia de erro
		if (ocorrenciaProbabilidade(ERRO_MENSAGEM)) {
			// a fim de proteger a referencia do Sender para 
			// a mensagem, criamos novo objeto e copiamos o estado
			Pacote m = new Pacote();
			m.setHeader(new String(msg.getHeader()));
			m.setBody(new String(msg.getBody().getBytes()).concat("ERRO"));
			m.setHash(new String(msg.getHash()));
			m.setSeq(msg.getSeq());
			if (msg.starting()) m.setStarting();
			if (msg.end()) m.setEnding();
			return m;
		}
		return msg;
	}

	private static boolean ocorrenciaProbabilidade(int problema) {
		// Distribuição de perdas. A quantidade de 1's
		// indica a probabilidade de perda de pacotes.
		// Assim, 2 valores 1 num array de 20 posicoes = 10% (2/20)
		
		int randomInt = randomGenerator.nextInt(20);

		if(problema == ERRO_ACK) {
			int[] a = { 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0 };
			return a[randomInt] == 1;
		} else if(problema == PERDA_PACOTE || problema == ERRO_MENSAGEM) {
			int[] a = { 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0 };
			return a[randomInt] == 1;
		} else if(problema == ATRASO_ACK) {
			int[] a = { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 };
			return a[randomInt] == 1;
		} else {
			return false;
		}
		
	}

	
	public static void ACK(long seq){
		
		ACK ack = new ACK(seq);
		
		if(errosACK) {
			if(ocorrenciaProbabilidade(ERRO_ACK)){
				ack.insertError();
			}
		}
		
		if(atrasoACK) {
			if(ocorrenciaProbabilidade(ATRASO_ACK)){
				inserirAtraso();
			}
		}
		
		try {
			controlChannel.put(ack);
//			System.out.println("[Control] " + ack.getMessage() + " with SEQ = " + ack.getSeq() + " sent from Receiver");
		} catch (InterruptedException e) {
			//nada a fazer se thread for interrompida
			return;
		}
	}
	
	private static void inserirAtraso() {
		try {
			System.out.println("[Control] Inicio de atraso no retorno do ACK...");
			Thread.sleep(atraso);
			System.out.println("[Control] Final de atraso. Liberando retorno do ACK.");
		} catch (InterruptedException e) {}
	}

//	public static ACK waitForConfirmation(){
//		try {
//			ACK ack = controlChannel.poll(timeout, TimeUnit.MILLISECONDS);
//			if (ack != null) {
//				System.out.println("[Control] " + ack.getMessage() + " with SEQ = " + ack.getSeq() + " received on Sender");
//			} else {
//				System.out.println("[Control] Timeout on Sender while waiting for a confirmation from the Receiver");
//			}
//			return ack;
//		} catch (InterruptedException e) {
//			return null;
//		}
//	}

	public static ACK getConfirmation(){
		ACK ack = controlChannel.poll();
//		if (ack != null) {
//			System.out.println("[Control] " + ack.getMessage() + " with SEQ = " + ack.getSeq() + " received on Sender");
//		} 
		return ack;
	}

	public static void flushChannel() {
		commChannel.clear();
	}

}
